# coding=utf-8
from __future__ import print_function, absolute_import, unicode_literals
from gm.api import *

import numpy as np
import pandas as pd
import multiprocessing
from sklearn.linear_model import LinearRegression

'''
本策略计算优加换手率因子，通过10分组回测筛选有效因子；
10分组回测基本思想：设定所需优化的参数数值范围及步长，将参数数值循环输入进策略，进行遍历回测，
                 记录每次回测结果和参数，根据某种规则将回测结果排序，找到最好的参数。
1、定义策略函数
2、多进程循环输入参数数值
3、获取回测报告，生成DataFrame格式
4、排序
'''


# 原策略中的参数定义语句需要删除！
def init(context):
    # 每月的第一个交易日的09:40:00执行策略algo
    schedule(schedule_func=algo, date_rule='1d', time_rule='14:50:00')
    # 设置交易标的
    context.symbol = None
    # 设置买入股票资金比例
    context.ratio = 1
    # 持股数量
    context.holding_num = 30


def MAD(data, N):
    """
    ---N倍中位数去极值---
    1 求所有因子的中位数 median
    2 求每个因子与中位数的绝对偏差值，求得到绝对偏差值的中位数 new_median
    3 根据参数 N 确定合理的范围为 [median?N*new_median,median+N*new_median]，并针对超出合理范围的因子值做调整
    """
    median = data.quantile(0.5)
    new_median = abs(data - median).quantile(0.5)
    # 定义N倍的中位数上下限
    high = median + N * new_median
    low = median - N * new_median
    # 替换上下限
    data = np.where(data > high, high, data)
    data = np.where(data < low, low, data)
    return data

def Standardize(data):
    """
    ---数据标准化---
    标准化后的数值 = (原始值 - 原始值均值) / 原始值标准差
    """
    mean = data.mean()
    std = data.std()
    return (data - mean) / std

def LR(x, y):
    '''
    ---建立回归方程---
    '''
    lr = LinearRegression()
    # 拟合
    lr.fit(x, y)
    # 预测
    y_predict = lr.predict(x)
    # 求残差
    data = y - y_predict
    return data

def algo(context):
    # 当前时间
    now_date = context.now.strftime('%Y-%m-%d')

    # 获取上一个交易日日期，即为上个月月底的日期
    last_date = get_previous_trading_date(exchange='SHSE', date=now_date)
    # 下一交易日
    next_day = get_N_trading_date(context,now_date,counts=1,model='next',exchange='SHSE')
    # 判断是否为每个月最后一个交易日
    if context.now.month!=pd.Timestamp(next_day).month:
        # 获取指数成分股
        stocks = get_history_constituents(index='SHSE.000852', start_date=last_date, end_date=last_date)\
                                        [0]['constituents'].keys()

        # 获取沪深300成分股过去20个交易日的换手率
        fundamentals = get_fundamentals_n_new(table='trading_derivative_indicator', symbols=list(stocks),
                                        end_date=last_date, count=20, fields='TURNRATE, NEGOTIABLEMV',df=True)

        # 对股票进行分组，计算每只股票过去20个交易日的换手率的均值、市值均值
        fundamental = fundamentals.groupby('symbol')[['TURNRATE', 'NEGOTIABLEMV']].mean()
        fundamental.columns = ['turn_mean', 'pe_mean']

        # 对股票进行分组，计算每只股票过去20个交易日的换手率的标准差
        fundamental['turn_std'] = fundamentals.groupby('symbol')['TURNRATE'].std()

        # 计算量小换手率因子：对每只股票过去20个交易日的换手率的均值，做市值中性化处理
        # 提取回归数据 x1: 市值, y1: 换手率均值
        fundamental['turn_mean'] = MAD(fundamental['turn_mean'], 3)
        fundamental['turn_mean'] = Standardize(fundamental['turn_mean'])
        x1 = fundamental['pe_mean'].values.reshape(-1, 1)
        y1 = fundamental['turn_mean']
        # 市值中性化处理得出量小换手率因子
        fundamental['turn_20'] = LR(x1, y1)

        # 计算量稳换手率因子：对每只股票过去20个交易日的换手率的标准差，做市值中性化处理
        # 提取回归数据 x2: 市值, y2: 换手率标准差
        fundamental['turn_std'] = MAD(fundamental['turn_std'], 3)
        fundamental['turn_std'] = Standardize(fundamental['turn_std'])
        x2 = fundamental['pe_mean'].values.reshape(-1, 1)
        y2 = fundamental['turn_std']
        # 市值中性化处理得出量稳换手率因子
        fundamental['str'] = LR(x2, y2)

        # 计算优加换手率：最终得分，即为优加换手率因子的因子值
        # 先将所有样本按照量稳因子从小到大排序，打分 1,2,……,N-1,N，N 为当期样本数量，记为“得分 1”；
        fundamental.sort_values(by='str', inplace=True)
        fundamental['score_1'] = range(1, len(fundamental) + 1)

        # 取量稳因子排名靠前的50%样本，再将它们按照量小因子从大到小排序，打分 1,2,…,N/2，记为“得分 2”；
        # 这些股票的最终得分为 “得分 1”+“得分 2”
        fund_1 = fundamental.iloc[:int(len(fundamental) / 2)]
        fund1 = fund_1.copy()
        fund1.sort_values(by='turn_20', ascending=False, inplace=True)
        fund1['score_2'] = range(1, len(fund1) + 1)
        fund1['scores'] = fund1['score_1'] + fund1['score_2']

        # 量稳因子排名靠后的50%样本，则将它们按照量小因子从小到大排序，打分 1,2,…,N/2，记为“得分 3”；
        # 这些股票的最终得分为 “得分 1”+“得分 3”
        fund_2 = fundamental.iloc[int(len(fundamental) / 2):]
        fund2 = fund_2.copy()
        fund2.sort_values(by='turn_20', inplace=True)
        fund2['score_3'] = range(1, len(fund2) + 1)
        fund2['scores'] = fund2['score_1'] + fund2['score_3']

        # 合并fund1、fund2，按照最终得分从小到大排序
        fundamental = pd.concat([fund1, fund2], join='inner')
        fundamental.sort_values(by='scores', inplace=True)

        # 进行10分组回测：第N次回测，对第N组的优加换手率进行回测，其中，对第N组中10%优加换手率较小的股票进行买入
        data = fundamental.iloc[:context.holding_num]
        to_buy = data.index.to_list()

        # 获取持仓
        positions = context.account().positions()
        holding_symbols = [posi['symbol'] for posi in positions]
        new_prices = history(symbol=holding_symbols, frequency='1d', start_time=now_date, end_time=now_date, fields='symbol,close', adjust=ADJUST_PREV, adjust_end_time=context.backtest_end_time, df=False)
        # 平不在标的池的股票（注：日频市价单，backtest_match_mode=1，以当日收盘价进行交易）
        for position in positions:
            symbol = position['symbol']
            if symbol not in to_buy:
                new_price = list(filter(lambda x:x[r'symbol'] == symbol, new_prices))
                if len(new_price)==0:continue
                order_target_percent(symbol=symbol, percent=0, order_type=OrderType_Market,position_side=PositionSide_Long,price=new_price[0]['close']*0.99)
                print('市价单平不在股票池的仓位', symbol)


        # 计算每只股票买入比例
        percent = context.ratio / len(to_buy)
        new_prices = history(symbol=to_buy, frequency='1d', start_time=now_date, end_time=now_date, fields='symbol,close', adjust=ADJUST_PREV, adjust_end_time=context.backtest_end_time, df=False)
        # 将股票池中的股票持仓调整至percent
        for symbol in to_buy:
            new_price = list(filter(lambda x:x[r'symbol'] == symbol, new_prices))
            if len(new_price)==0:continue
            order_target_percent(symbol=symbol, percent=percent, order_type=OrderType_Market,
                                position_side=PositionSide_Long, price=new_price[0]['close']*1.01)
            print('以市价单调仓至买入比例', symbol)


def get_fundamentals_n_new(table, symbols, end_date, fields,count, df):
    df_total = pd.DataFrame()
    for i in range(len(symbols)//200+1):
        security_ = symbols[i*200:(i+1)*200]
        df_ = get_fundamentals_n(table=table, symbols=security_, end_date=end_date, fields=fields,count=count, df=df)
        df_total = pd.concat([df_total,df_])
    return df_total

# 获取每次回测的报告数据
def on_backtest_finished(context, indicator):
    print(indicator)


if __name__ == '__main__':
    '''
        strategy_id策略ID,由系统生成
        filename文件名,请与本文件名保持一致
        mode实时模式:MODE_LIVE回测模式:MODE_BACKTEST
        token绑定计算机的ID,可在系统设置-密钥管理中生成
        backtest_start_time回测开始时间
        backtest_end_time回测结束时间
        backtest_adjust股票复权方式不复权:ADJUST_NONE前复权:ADJUST_PREV后复权:ADJUST_POST
        backtest_initial_cash回测初始资金
        backtest_commission_ratio回测佣金比例
        backtest_slippage_ratio回测滑点比例
    '''
    run(strategy_id='859c3b0a-1765-11ec-acc4-d0509949eed8',
        filename='main.py',
        mode=MODE_BACKTEST,
        token='47ca47f849b3a0f66ec0f7013bb56bb667d63a70',
        backtest_start_time='2018-12-28 08:00:00',
        backtest_end_time='2023-10-08 16:00:00',
        backtest_adjust=ADJUST_PREV,
        backtest_initial_cash=1000000,
        backtest_commission_ratio=0.0007,
        backtest_slippage_ratio=0.00123,
        backtest_match_mode=1)